﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTK;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Input;
using System.Drawing;
using OpenTK.Graphics;
using Common;
using AntTweakBar;

namespace T19_Misc05_2_picking_custom {
    class T19_Misc05_2_picking_customGameWindow : GameWindow {
        public T19_Misc05_2_picking_customGameWindow() {
            this.Load += T19_Misc05_2_picking_customGameWindow_Load;
            this.Resize += T19_Misc05_2_picking_customGameWindow_Resize;
            this.RenderFrame += T19_Misc05_2_picking_customGameWindow_RenderFrame;
            this.Unload += T19_Misc05_2_picking_customGameWindow_Unload;
            this.Width = 1024;
            this.Height = 768;
            this.Title = "T19_Misc05_2_picking_customGameWindow";

        }

        void ScreenPosToWorldRay(
    int mouseX, int mouseY,             // Mouse position, in pixels, from bottom-left corner of the window
    int screenWidth, int screenHeight,  // Window size, in pixels
    Matrix4 ViewMatrix,               // Camera position and orientation
    Matrix4 ProjectionMatrix,         // Camera parameters (ratio, field of view, near and far planes)
    out Vector3 out_origin,              // Ouput : Origin of the ray. /!\ Starts at the near plane, so if you want the ray to start at the camera's position instead, ignore this.
    out Vector3 out_direction            // Ouput : Direction, in world space, of the ray that goes "through" the mouse.
) {

            // The ray Start and End positions, in Normalized Device Coordinates (Have you read Tutorial 4 ?)
            Vector4 lRayStart_NDC=new Vector4(
                ((float) mouseX/ (float)screenWidth - 0.5f) *2.0f, // [0,1024] -> [-1,1]
		((float)mouseY / (float)screenHeight - 0.5f) * 2.0f, // [0, 768] -> [-1,1]
		-1.0f, // The near plane maps to Z=-1 in Normalized Device Coordinates
		1.0f
	);
            Vector4 lRayEnd_NDC=new Vector4(
                ((float) mouseX/ (float)screenWidth - 0.5f) *2.0f,
		((float)mouseY / (float)screenHeight - 0.5f) * 2.0f,
		0.0f,
		1.0f
	);


            // The Projection matrix goes from Camera Space to NDC.
            // So inverse(ProjectionMatrix) goes from NDC to Camera Space.
            //Matrix4 InverseProjectionMatrix = glm::inverse(ProjectionMatrix);
            Matrix4 InverseProjectionMatrix = ProjectionMatrix.Inverted();

            // The View Matrix goes from World Space to Camera Space.
            // So inverse(ViewMatrix) goes from Camera Space to World Space.
            Matrix4 InverseViewMatrix = ViewMatrix.Inverted();

            Vector4 lRayStart_camera =lRayStart_NDC  * InverseProjectionMatrix; lRayStart_camera /= lRayStart_camera.W;
            Vector4 lRayStart_world = lRayStart_camera *InverseViewMatrix ; lRayStart_world /= lRayStart_world.W;
            Vector4 lRayEnd_camera =  lRayEnd_NDC* InverseProjectionMatrix; lRayEnd_camera /= lRayEnd_camera.W;
            Vector4 lRayEnd_world =  lRayEnd_camera*InverseViewMatrix ; lRayEnd_world /= lRayEnd_world.W;


            // Faster way (just one inverse)
            //glm::mat4 M = glm::inverse(ProjectionMatrix * ViewMatrix);
            //glm::vec4 lRayStart_world = M * lRayStart_NDC; lRayStart_world/=lRayStart_world.w;
            //glm::vec4 lRayEnd_world   = M * lRayEnd_NDC  ; lRayEnd_world  /=lRayEnd_world.w;


           Vector3 lRayDir_world=new Vector3(lRayEnd_world -lRayStart_world);
            //lRayDir_world = glm::normalize(lRayDir_world);
            lRayDir_world =lRayDir_world.Normalized();


            out_origin = new Vector3(lRayStart_world);
            //out_direction = glm::normalize(lRayDir_world);
            out_direction = lRayDir_world.Normalized();
        }

        bool TestRayOBBIntersection(
    Vector3 ray_origin,        // Ray origin, in world space
    Vector3 ray_direction,     // Ray direction (NOT target position!), in world space. Must be normalize()'d.
    Vector3 aabb_min,          // Minimum X,Y,Z coords of the mesh when not transformed at all.
    Vector3 aabb_max,          // Maximum X,Y,Z coords. Often aabb_min*-1 if your mesh is centered, but it's not always the case.
    Matrix4 ModelMatrix,       // Transformation applied to the mesh (which will thus be also applied to its bounding box)
    out float intersection_distance // Output : distance between ray_origin and the intersection with the OBB
) {
            intersection_distance = float.MinValue;
            // Intersection method from Real-Time Rendering and Essential Mathematics for Games

            float tMin = 0.0f;
            float tMax = 100000.0f;

            Vector3 OBBposition_worldspace=new Vector3(ModelMatrix.Row3.X, ModelMatrix.Row3.Y, ModelMatrix.Row3.Z);

            Vector3 delta = OBBposition_worldspace - ray_origin;

            // Test intersection with the 2 planes perpendicular to the OBB's X axis
            {
                Vector3 xaxis=new Vector3(ModelMatrix.Row0.X, ModelMatrix.Row0.Y, ModelMatrix.Row0.Z);
                float e = Vector3.Dot(xaxis, delta);
                float f = Vector3.Dot(ray_direction, xaxis);

                if (Math.Abs(f) > 0.001f) { // Standard case

                    float t1 = (e + aabb_min.X) / f; // Intersection with the "left" plane
                    float t2 = (e + aabb_max.X) / f; // Intersection with the "right" plane
                                                     // t1 and t2 now contain distances betwen ray origin and ray-plane intersections

                    // We want t1 to represent the nearest intersection, 
                    // so if it's not the case, invert t1 and t2
                    if (t1 > t2) {
                        float w = t1; t1 = t2; t2 = w; // swap t1 and t2
                    }

                    // tMax is the nearest "far" intersection (amongst the X,Y and Z planes pairs)
                    if (t2 < tMax)
                        tMax = t2;
                    // tMin is the farthest "near" intersection (amongst the X,Y and Z planes pairs)
                    if (t1 > tMin)
                        tMin = t1;

                    // And here's the trick :
                    // If "far" is closer than "near", then there is NO intersection.
                    // See the images in the tutorials for the visual explanation.
                    if (tMax < tMin)
                        return false;

                }
                else { // Rare case : the ray is almost parallel to the planes, so they don't have any "intersection"
                    if (-e + aabb_min.X > 0.0f || -e + aabb_max.X < 0.0f)
                        return false;
                }
            }


            // Test intersection with the 2 planes perpendicular to the OBB's Y axis
            // Exactly the same thing than above.
            {
                Vector3 yaxis=new Vector3(ModelMatrix.Row1.X, ModelMatrix.Row1.Y, ModelMatrix.Row1.Z);
                float e =Vector3.Dot(yaxis, delta);
                float f = Vector3.Dot(ray_direction, yaxis);

                if (Math.Abs(f) > 0.001f) {

                    float t1 = (e + aabb_min.Y) / f;
                    float t2 = (e + aabb_max.Y) / f;

                    if (t1 > t2) { float w = t1; t1 = t2; t2 = w; }

                    if (t2 < tMax)
                        tMax = t2;
                    if (t1 > tMin)
                        tMin = t1;
                    if (tMin > tMax)
                        return false;

                }
                else {
                    if (-e + aabb_min.Y > 0.0f || -e + aabb_max.Y < 0.0f)
                        return false;
                }
            }


            // Test intersection with the 2 planes perpendicular to the OBB's Z axis
            // Exactly the same thing than above.
            {
                Vector3 zaxis=new Vector3(ModelMatrix.Row2.X, ModelMatrix.Row2.Y, ModelMatrix.Row2.Z);
                float e = Vector3.Dot(zaxis, delta);
                float f = Vector3.Dot(ray_direction, zaxis);

                if (Math.Abs(f) > 0.001f) {

                    float t1 = (e + aabb_min.Z) / f;
                    float t2 = (e + aabb_max.Z) / f;

                    if (t1 > t2) { float w = t1; t1 = t2; t2 = w; }

                    if (t2 < tMax)
                        tMax = t2;
                    if (t1 > tMin)
                        tMin = t1;
                    if (tMin > tMax)
                        return false;

                }
                else {
                    if (-e + aabb_min.Z > 0.0f || -e + aabb_max.Z < 0.0f)
                        return false;
                }
            }

            intersection_distance = tMin;
            return true;

        }

        private void T19_Misc05_2_picking_customGameWindow_RenderFrame(object sender, FrameEventArgs e) {
            GL.ClearColor(.1f, .1f, .99f, .9f);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            Render();
            context.Draw();
            SwapBuffers();
            ProcessEvents();
        }

        const int objectCount = 100;
        Vector3[] positions = new Vector3[objectCount];
        Quaternion[] orientations = new Quaternion[objectCount];

        int rand() {
            return random.Next();
        }
        Random random = new Random();

        private void InitializePosOrient() {
            for (int i = 0; i < positions.Length; i++) {
                positions[i] = new Vector3(rand() % 20 - 10, rand() % 20 - 10, rand() % 20 - 10);
                orientations[i] = new Quaternion(rand() % 360, rand() % 360, rand() % 360);
            }
        }

        Context context;
        StringVariable messageVar;

        private void InitializeBars() {
            context = new Context(Tw.GraphicsAPI.OpenGL);
            Bar GUI = new Bar(context) { Label = "Picking", ValueColumnWidth = 100, Size = new Size(340, 200) };
            GUI.SetDefinition("refresh=0.1");
            messageVar = new StringVariable(GUI) { Label = "Last picked object", };
            messageVar.Value = "test";
        }
        protected override void OnResize(EventArgs e) {
            base.OnResize(e);
            context.HandleResize(this.ClientSize);
        }

        Vao vao;
        Shader shader;
        ModelMesh mesh;
        Texture2D texture;
        VertexAttribManager attribs = new VertexAttribManager();
        private void T19_Misc05_2_picking_customGameWindow_Load(object sender, EventArgs e) {

            InitializeBars();
            VSync = VSyncMode.Off;
            GL.Enable(EnableCap.DepthTest);
            GL.DepthFunc(DepthFunction.Less);
            GL.Enable(EnableCap.CullFace);

            vao = new Vao();
            shader = new Shader("shader.vs", "shader.fs");

            mesh = MeshLoader.LoadObjIndexed("suzanne.obj");
            mesh.CreateVbo();

            texture = Texture2D.Load("uvmap.dds");
            InitializePosOrient();

            attribs.AddAttrib(3, mesh.VertexBuffer);
            attribs.AddAttrib(2, mesh.UvBuffer);
            attribs.AddAttrib(3, mesh.NormalBuffer);

           
        }

        private void Render() {
            InputControls.computeMatricesFromInput(this);
            var projection = InputControls.ProjectionMatrix;
            var view = InputControls.ViewMatrix;

            //view = Matrix4.Identity;
            //positions[0] = new Vector3(1, 2, 3);
            //orientations[0] = new Quaternion(4, 5, 6, 7);

            if (InputControls.isMouseButtonDown(MouseButton.Left)) {
                ScreenPosToWorldRay(this.Width / 2, this.Height / 2,
                    this.Width, this.Height,
                    view,
                    projection,
                    out var ray_origin,
                    out var ray_direction);

                messageVar.Value = "background";

                // Test each each Oriented Bounding Box (OBB).
                // A physics engine can be much smarter than this, 
                // because it already has some spatial partitionning structure, 
                // like Binary Space Partitionning Tree (BSP-Tree),
                // Bounding Volume Hierarchy (BVH) or other.
                for (int i = 0; i < positions.Length; i++) {

                    float intersection_distance; // Output of TestRayOBBIntersection()
                    Vector3 aabb_min = new Vector3(-1.0f, -1.0f, -1.0f);
                    Vector3 aabb_max = new Vector3(1.0f, 1.0f, 1.0f);

                    // The ModelMatrix transforms :
                    // - the mesh to its desired position and orientation
                    // - but also the AABB (defined with aabb_min and aabb_max) into an OBB
                    //Matrix4 RotationMatrix = glm::toMat4(orientations[i]);
                    Matrix4 RotationMatrix = Matrix4.CreateFromQuaternion(orientations[i]);

                    Matrix4 TranslationMatrix = Matrix4.CreateTranslation(positions[i]);
                    Matrix4 ModelMatrix = RotationMatrix * TranslationMatrix;


                    if (TestRayOBBIntersection(
                        ray_origin,
                        ray_direction,
                        aabb_min,
                        aabb_max,
                        ModelMatrix,
                        out intersection_distance)
                    ) {
                        //std::ostringstream oss;
                        //oss << "mesh " << i;
                        //message = oss.str();
                        messageVar.Value = $"mesh {i}";
                        break;
                    }
                }

            }//endif

            GL.ClearColor(0, 0, .4f, 0);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            shader.Use();

            for (int i = 0; i < positions.Length; i++) {
                var rotationMatrix = Matrix4.CreateFromQuaternion(orientations[i]);
                var TranslationMatrix = Matrix4.CreateTranslation(positions[i]);
                //glm::mat4 ModelMatrix = TranslationMatrix * RotationMatrix;
                var modelMatrix = rotationMatrix * TranslationMatrix;

                var mvp = modelMatrix * view * projection;

                shader.SetMatrix4("MVP", ref mvp);
                shader.SetMatrix4("M", ref modelMatrix);
                shader.SetMatrix4("V", ref view);

                shader.SetVector3("LightPosition_worldspace", 4, 4, 4);

                texture.Active(TextureUnit.Texture0, shader, "myTextureSampler");
                attribs.Enable();
                mesh.IndexBuffer.Bind();
                GL.DrawElements(BeginMode.Triangles, mesh.Indices.Length, DrawElementsType.UnsignedShort, 0);
                attribs.Disable();

            }
        }

        private void T19_Misc05_2_picking_customGameWindow_Resize(object sender, EventArgs e) {
            GL.Viewport(0, 0, Width, Height);
        }



        private void T19_Misc05_2_picking_customGameWindow_Unload(object sender, EventArgs e) {
            GlDisposableObjectBase.DisposeAllDisposableMembers(this);
        }

    }
}
